/** This class loads all available plugins. 
 *  @author Copyright (c) 2004-2005 by chrmac@gmx.de */

package com.jtalker.client;

import java.io.File;
import java.io.FilenameFilter;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * This class provides an access interface for all plugins.
 * 
 * @author ChrM
 */
public class JTalkerClientPluginLoader {
	private JTalkerClientFront parent;
	private JTalkerClientPlugInInterface allPlugins[];
	public static final String PLUGIN_NAMESPACE = "jtplugin";
	private String lastErrorMsg = null;

	/** C'tor */
	public JTalkerClientPluginLoader(JTalkerClientFront parent) {
		this.parent = parent;
	}

	/** Initializes all plugins */
	public boolean initPlugins(String searchURL) {
		// De-register from command responses first
		if (allPlugins != null) {
			for (int i = 0; i < allPlugins.length; i++) {
				System.out.println("[PLUGIN-LOADER]: Unloading: " + allPlugins[i].getName());
				allPlugins[i].unload();
			}
		}

		if (!this.loadPlugIns(searchURL)) {
			System.err.println("*** " + this.lastErrorMsg);
			System.err.println("*** Can't init plugins. Disable.");
			return false;
		}
		System.out.println("[PLUGIN-LOADER]: Number of loaded plugins:" + allPlugins.length);

		return true;
	}

	/** If an error occurs, this method gets an error string. Otherwise it returns null. */
	public String getLastError() {
		String tmpRetVal = this.lastErrorMsg;
		this.lastErrorMsg = null;
		return tmpRetVal;
	}

	// ////////////////////////// Loading Plugins ////////////////////////////////////

	/** Gets a list of loaded plugin class names */
	public String[] getPluginClassNames() {
		if (allPlugins == null || allPlugins.length == 0) {
			return null;
		}
		String[] retVal = new String[allPlugins.length];

		for (int i = 0; i < allPlugins.length; i++) {
			retVal[i] = allPlugins[i].getClass().getName();
		}

		return retVal;
	}

	/** Gets a list of loaded plugin names */
	public String[] getPluginNames() {
		if (allPlugins == null || allPlugins.length == 0) {
			return null;
		}
		String[] retVal = new String[allPlugins.length];
		for (int i = 0; i < allPlugins.length; i++) {
			retVal[i] = allPlugins[i].getName();
		}

		return retVal;
	}

	/**
	 * Shows the main GUI for a given Plugin
	 * 
	 * @param pluginName
	 *            Plugins Name
	 * @param nameType
	 *            type of the name. 0: Class 1: real name
	 */
	public boolean showPluginsGUI(String pluginName, int nameType) {
		try {
			if (nameType == 0) {
				this.getPluginByClassName(pluginName).getMainFrame().setVisible(true);
			} else {
				this.getPluginByName(pluginName).getMainFrame().setVisible(true);
			}
		} catch (Exception e) {
			return false;
		}

		return true;
	}

	/**
	 * Shows the settings GUI for a given Plugin
	 * 
	 * @param pluginName
	 *            Plugins Name
	 * @param nameType
	 *            type of the name. 0: Class 1: real name
	 */
	public boolean showPluginsSettings(String pluginName, int nameType) {
		try {
			if (nameType == 0) {
				this.getPluginByClassName(pluginName).getSettingsFrame().setVisible(true);
			} else {
				this.getPluginByName(pluginName).getSettingsFrame().setVisible(true);
			}
		} catch (Exception e) {
			return false;
		}

		return true;
	}

	/** Gets Plugin-Object by class name */
	public JTalkerClientPlugInInterface getPluginByClassName(String className) {
		JTalkerClientPlugInInterface retVal = null;

		for (int i = 0; i < allPlugins.length; i++) {
			retVal = allPlugins[i];
			if (retVal.getClass().getName().equals(className)) {
				return retVal;
			}
		}

		return null;
	}

	/** Gets the first Plugin-Object matches the name */
	public JTalkerClientPlugInInterface getPluginByName(String name) {
		JTalkerClientPlugInInterface retVal = null;
		for (int i = 0; i < allPlugins.length; i++) {
			retVal = allPlugins[i];
			if (retVal.getName().equals(name)) {
				return retVal;
			}
		}
		return null;
	}

	/**
	 * Tryes to load all plugins in the given directory
	 * 
	 * @param fromDir
	 *            Search for plugins in this directory
	 * @return False, if something went wrong.
	 * @see getLastError()
	 */

	private boolean loadPlugIns(String fromDir) {
		// Check, if its a directory
		if (!new File(fromDir).isDirectory()) {
			this.lastErrorMsg = "Not a directory: " + fromDir;
			return false;
		}

		// Search for jar-files and init plugins
		String searchURL = fromDir;

		// Parse directory content for "*.jar"
		String[] allPluginFiles = getPlugInNames(searchURL);

		// Found nothing
		if (allPluginFiles == null) {
			this.lastErrorMsg = "No plugins found in " + searchURL;
			return false;
		}

		// Create array
		JTalkerClientPlugInInterface[] tmpAllPlugins = new JTalkerClientPlugInInterface[allPluginFiles.length];
		int cnt = 0; // Real number of successfully loaded plugins

		try {
			// Pre-load all PlugIns
			URL[] urlsToLoadFrom = new URL[allPluginFiles.length];

			for (int i = 0; i < allPluginFiles.length; i++) {
				URI tmpURI = new File(searchURL + allPluginFiles[i]).toURI();
				urlsToLoadFrom[i] = tmpURI.toURL();
				System.out.println("+ Searching in: " + urlsToLoadFrom[i].toString());
			}

			// URL[] urlsToLoadFrom = new URL[]{new File(searchURL).toURL()};
			URLClassLoader loader = new URLClassLoader(urlsToLoadFrom);

			Class tmpClass;

			for (int i = 0; i < allPluginFiles.length; i++) {
				try {
					tmpClass = Class.forName(
							"com.jtalker.client.plugins.TicTacToe." + allPluginFiles[i].replaceAll("\\.class", ""),
							true, loader);

					JTalkerClientPlugInInterface tmpObj = (JTalkerClientPlugInInterface) tmpClass.newInstance();

					if (tmpObj != null) {
						tmpAllPlugins[i] = tmpObj;
						tmpAllPlugins[i].setParentObject(new JTalkerClientPluginConnector(this.parent));
						System.out.println("+ Init Plugin: " + tmpAllPlugins[i].getName() + " --> OK.");
						cnt++;
					} else {
						System.err.println("*** Can't init plugin: " + allPluginFiles[i] + ". Disable this plugin.");
					}
				} catch (Exception e1) {
					System.err.println("*** Can't init plugin: " + allPluginFiles[i] + ". Disable this plugin.");
					// e1.printStackTrace();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			// Disable plugins
			this.allPlugins = null;
			this.lastErrorMsg = "Exception: " + e.toString();
			return false;
		}

		// Copy all loaded plugins to the allPluginsArray
		this.allPlugins = new JTalkerClientPlugInInterface[cnt];

		int j = 0;

		for (int i = 0; i < tmpAllPlugins.length; i++) {
			if (tmpAllPlugins[i] != null) {
				this.allPlugins[j] = tmpAllPlugins[i];
				j++;
			}
		}

		return true;
	}

	/**
	 * Gets an array of PlugIn names in given URL or null.
	 * 
	 * @param searchURL
	 *            PlugIn directory. If null, use "./plugins"
	 */
	private static String[] getPlugInNames(String searchURL) {
		if (searchURL == null) {
			searchURL = "./plugins";
		}
		File dir = new File(searchURL);

		FilenameFilter filter = new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(".class");
			}
		};

		String plugins[] = dir.list(filter);

		if (plugins == null) {
			return null;
		}

		return plugins;
	}

	// /////////////// Invoking methods ////////////////////////

	/**
	 * Parses and invokes a method if plugIn is available, otherwise it returns null
	 * 
	 * @param methodContent
	 *            Method for a PlugIn. The String looks like this: plugin::method("parameter")
	 * @param caller
	 *            User who call this method
	 * @return The result of the method or null if somthing went wrong - in this case read the last error.
	 * @see getLastError()
	 */

	public synchronized String invokePlugInMethod(String methodContent, String caller) {
		// Reset ErrorMsg
		this.lastErrorMsg = null;
		methodContent = methodContent.trim();

		System.out.println("+ Invoking: " + methodContent + ", recieved from: " + caller);

		try {

			if (methodContent == null || methodContent.indexOf("::") == -1) {
				this.lastErrorMsg = "Syntax error. Try module::method!";
				return null;
			}

			String[] tmpValues = methodContent.split("::");

			if (tmpValues.length != 2) {
				this.lastErrorMsg = "Syntax error. Try module::method!";
				return null;
			}

			String plugin = tmpValues[0];
			String method = tmpValues[1];
			JTalkerClientPlugInInterface tmpPlugInObj = null;

			if (plugin == null || plugin.equals("") || method == null || method.equals("")) {
				this.lastErrorMsg = "Syntax error";
				return null;
			}

			// Std-Server-Method?
			if (plugin.equalsIgnoreCase("std")) {
				return invokeBuildInMethod(method, caller);
			}

			if (this.allPlugins == null) {
				this.lastErrorMsg = "No plugin loaded.";
				return null;
			}

			// Check if plugin is available
			boolean hit = false;

			for (int i = 0; i < this.allPlugins.length; i++) {
				if (plugin.endsWith(this.allPlugins[i].getClass().getSimpleName())) {
					hit = true;
					tmpPlugInObj = this.allPlugins[i];
					break;
				}
			}

			if (!hit) {
				this.lastErrorMsg = "Plugin is not available on this client: " + plugin;
				return null;
			}

			// TODO: Call plugins method here!

			try {
				Method[] plugInsMethods = tmpPlugInObj.getCommandMethods();
				method = method.trim();

				// Say PlugIn who's calling
				// tmpPlugInObj.setCaller(_caller.getUserName(), _caller.getUserIP());

				// Gets all available commands for remote using
				if (method.equals("getMethods()")) {
					String methodNamesInLine = "";

					for (int i = 0; i < tmpPlugInObj.getCommandMethods().length; i++) {
						methodNamesInLine += tmpPlugInObj.getCommandMethods()[i].toString();
						methodNamesInLine += ";;";
					}

					return methodNamesInLine;

				} else {
					// Parse and invoke special plug-in method
					System.out.println("+ Using Plugin: " + tmpPlugInObj.getName() + " caller: " + caller);
					String tmpMethodName = (method.substring(0, method.indexOf('('))).trim();

					String[] params = JTalkerClientPluginLoader.parseParams(method);

					for (int i = 0; i < plugInsMethods.length; i++) {
						if (plugInsMethods[i].getName().equals(tmpMethodName)) {
							// HIT!
							String retVal = (String) plugInsMethods[i].invoke(tmpPlugInObj, params);

							// Reset callers data
							// tmpPlugInObj.setCaller(null, null);

							return retVal;
						}
					}

					this.lastErrorMsg = "Method not declared: " + method;
					return null;
				}

			} catch (Exception _e) {
				System.out.println("*** ERROR: Can't invoke PlugIn method (" + plugin + "::" + method + "): "
						+ _e.toString());
				this.lastErrorMsg = "Can't invoke method: " + method + ". " + _e.toString();
				return null;
			}

		} catch (Exception _e) {
			_e.printStackTrace();
			this.lastErrorMsg = "Can't invoke method: " + methodContent + ". " + _e.toString();
		}

		this.lastErrorMsg = "Unexpected Error. Can't invoke this method";
		return null;
	}

	/**
	 * Invoke Buildin Method
	 * 
	 * @param _method
	 *            Method to invoke
	 * @param _caller
	 *            User who call this method
	 * @return Return value of the invoked method
	 * */
	private String invokeBuildInMethod(String _method, String _caller) {
		// remove spaces
		_method = _method.replaceAll(" ", "");

		if (_method.equals("getClientInfo()")) {
			// return this.parent.getClientInfo();
		}

		return "Unknown method: " + _method;
	}

	/** Parsing method parameter */
	private static String[] parseParams(String method) {

		String tmpParameters = method.substring(method.indexOf('(') + 1, method.lastIndexOf(')'));
		String[] params = tmpParameters.split("\"[\\s]*,[\\s]*\"");

		if (params == null || params.length < 1 || (params.length == 1 && params[0].equals(""))) {
			return null;
		} else {
			params[0] = params[0].replaceFirst("\"", "");
			params[params.length - 1] = params[params.length - 1].replaceFirst("\"", "");
		}

		return params;
	}

}
